﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.ComponentModel.DataAnnotations;

namespace BoldBrick.Web.Parameters
{
    /// <summary>
    /// In-memory cache and provider of <see cref="PageQueryInfo"/> objects.
    /// </summary>
    static class PageQueryParametersCache
    {
        #region ------ Constructors ---------------------------------------------------------------

        /// <summary>
        /// Static constructor initializing <see cref="PageQueryInfo"/> objects cache
        /// </summary>
        static PageQueryParametersCache()
        {
            // creates new blank dictionary
            cachedParamInfos = new Dictionary<Type, PageQueryInfo>();
        }

        #endregion

        /// <summary>
        /// Method looks for PageQueryInfo definition for specific type, if not found in cache, inspects the type and remember
        /// </summary>
        /// <param name="pageQueryType">Type of page query data object</param>
        /// <returns>PageQueryInfo according to type of page query data object</returns>
        public static PageQueryInfo GetParametersInfo(Type pageQueryType)
        {
            if (pageQueryType == null)
                throw new ArgumentNullException("pageQueryType");

            PageQueryInfo queryInfo;
            // tries to get known object from cache
            if (cachedParamInfos.TryGetValue(pageQueryType, out queryInfo))
                return queryInfo;

            // prevent paralel operations
            lock (typeof(PageQueryParametersCache))
            {
                // another try in locked block
                if (cachedParamInfos.TryGetValue(pageQueryType, out queryInfo))
                    return queryInfo;

                // get new query info object for specific type
                queryInfo = InspectParametersType(pageQueryType);

                // add query info to cache for later faster work
                cachedParamInfos.Add(pageQueryType, queryInfo);

                return queryInfo;
            }
        }

        #region ------ Internals ------------------------------------------------------------------

        /// <summary>
        /// Holds collections of previously loaded <see cref="PageQueryInfo"/> objects
        /// </summary>
        private static IDictionary<Type, PageQueryInfo> cachedParamInfos;

        /// <summary>
        /// Inspects query data type and construct information object about how to parse parameters from query
        /// </summary>
        /// <param name="pageQueryType">Type of page query data object</param>
        /// <returns>new PageQueryInfo according to type of page query data object, returns null if no parameters found on type</returns>
        private static PageQueryInfo InspectParametersType(Type pageQueryType)
        {
            if (pageQueryType == null)
                throw new ArgumentNullException("pageQueryType");

            IList<QueryParameter> parLst = new List<QueryParameter>();
            // iterates through type properties
            foreach (PropertyInfo propInfo in pageQueryType.GetProperties())
            {
                // filter only on ParameterNameAttributes which identify query parameters on type
                object[] paramNameAttrs = propInfo.GetCustomAttributes(typeof(ParameterNameAttribute), false);
                if (paramNameAttrs.Length > 0)
                {
                    if (paramNameAttrs.Length > 1)
                        throw new InvalidOperationException("Multiple parameters found.");

                    ParameterNameAttribute pNameAttr = (ParameterNameAttribute)paramNameAttrs[0];

                    // process validation attributes
                    bool isRequired = false;
                    object[] validAttrs = propInfo.GetCustomAttributes(typeof(ValidationAttribute), false);
                    if (validAttrs.Any(a => a.GetType() == typeof(RequiredAttribute)))
                        isRequired = true;

                    // proces display format attributes
                    string dataFormatString = null;
                    object[] dfsAttrs = propInfo.GetCustomAttributes(typeof(DisplayFormatAttribute), false);
                    DisplayFormatAttribute dftAtt = (DisplayFormatAttribute)dfsAttrs.SingleOrDefault();
                    if (dftAtt != null)
                        dataFormatString = dftAtt.DataFormatString;

                    // add new parameter to list
                    parLst.Add(
                        new QueryParameter
                            {
                                FieldInfo = propInfo,
                                ParamName = pNameAttr.Name,
                                IsRequired = isRequired,
                                DataFormatString = dataFormatString,
                            });
                }
            }

            // if no parameters found on type, returns null
            if (parLst.Count == 0)
                return null;

            // creates new page info object
            PageQueryInfo pInfo = new PageQueryInfo();
            pInfo.ParametersType = pageQueryType;
            pInfo.Parameters = parLst.ToArray();
            return pInfo;
        }

        #endregion
    }
}
